DrawCall
- DrawCall的含义是CPU调用图像编程接口,如OpenGL中的glDraw Elements命令或者DirectX中的DrawIndexedPrimitive命令,命令GPU进行渲染操作。
- CPU和GPU是并行操作的,CPU渲染对象时向命令缓冲区添加命令,当GPU完成上一次的渲染任务后,会从命令缓冲区队列中取出一个命令执行。
- Unity的合并原理(包括UGUI)是将相同的Material(参数也需要一样,如Shader和Texture)的节点进行Mesh的Combine操作。因此UGUI减少DrawCall的方式是尽可能地进行动态合批,而每次合批都会产生很大的代价。比如合并Mesh后对Renderer进行设置执行Renderer.material就会new出一个新的Material,这个Material会包含合并后的Texture信息。
UGUI DrawCall计算原理
用过NGUI的都知道Depth,只不过UGUI的Depth的动态计算出来的。通过Depth的大小基本的确定DrawCall的值。
计算Depth
- 按照Hierarchy窗口中节点的顺序,从上到下进行Depth分析(深度优先原则)。
- 跳过那些不渲染的节点,比如GameObject.activeSelf=false,Image.enabled = false,Canvas不渲染的Layer等。
- 如果处于渲染状态并且没有其他渲染元素与其相交,Depth=0。注意,不是RectTransform的Rect相交,而是渲染元素本身相交,如下图所示:
- 如果处于渲染元素并且有其他渲染元素与其相交:
- 找到相交的渲染元素中最大Depth值MaxDepth的渲染元素。
- 判断是否能与其Batch(合批),如果能Batch,则Depth=MaxDepth,否则Depth=MaxDepth+1。
- 如果一个渲染元素同时覆盖多个其他渲染元素,则选取Depth值最高的渲染元素,然后回到步骤4。
- 这里Depth值是按照Hierarchy顺序计算的,如果渲染元素A盖住了渲染元素B,而B的Depth值还没有计算,这时是无法计算渲染元素A的。(这种情况是不会出现的,因为被盖住的渲染元素B总会有值,等渲染元素B计算完Depth值,才会计算渲染元素A的Depth值)
注:从上面的规则中可以得出,Depth值与是否相交有关,与是否为子节点无关。
对Depth进行升序排列
如下图所示,对Depth值进行升序排列:
计算Depth值并排序:
节点名称 | Depth排序前 | 节点名称 | Depth排序后 | |
---|---|---|---|---|
Image1 | 0 | Image1 | 0 | |
Text1 | 0 | Text1 | 0 | |
Image2 | 0 | Image2 | 0 | |
Text2 | 1 | Image3 | 0 | |
Image3 | 0 | Text2 | 1 | |
Text3 | 1 | Text3 | 1 |
获得材质球Material的InstanceID后进行升序排列,注意此时排序只针对Depth值相同的情况
节点名称 | MatID排序前 | 节点名称 | MatID排序后 | |
---|---|---|---|---|
Image1 | -1276 | Image1 | -1276 | |
Text1 | -1276 | Text1 | -1276 | |
Image2 | -1276 | Image2 | -1276 | |
Image3 | -1276 | Image3 | -1276 | |
Text2 | -1276 | Text2 | -1276 | |
Text3 | -1276 | Text3 | -1276 |
从排序节点中可以看出所有节点的MatID都一样,这是因为很多情况都使用的Default UI Material。
获得纹理Texture的InstanceID,并进行升序排列
- 使用Sprite Atlas图集或者TP打的图集,需要游戏运行起来,TextureID就是图集的ID。
- 使用单个图片没有图集,TextureID就是其本身的ID。
- 使用同一个字体的Text,TextureID就是其字体的ID。
- 此时MatID的顺序不会发生改变,但是可以通过TextureID,影响MatID和Depth都相同的节点排序。
注:可以使用Image.mainTexture.GetInstanceID()和Text.mainTexture.GetInstanceID()来获取TextureID。
节点名称 | Depth值 | MatID值 | TextureID排序前 | 节点名称 | TextureID排序后 | |
---|---|---|---|---|---|---|
Image1 | 0 | -1758 | 10576 | Text1 | 522 | |
Image3 | 0 | -1758 | 2628 | Image3 | 2628 | |
Text1 | 0 | -1758 | 522 | Image1 | 10576 | |
Image2 | 0 | -1758 | 10576 | Image2 | 10576 | |
Text2 | 1 | -1758 | 522 | Text2 | 522 | |
Text3 | 1 | -1758 | 522 | Text3 | 522 |
- 通过排序得出最后的DrawCall,即522/2628/10576/522,4个DrawCall。
分析一个复杂的例子
- UI层级结构。
- 基础信息。
节点名称 Mat名称 MatID TextureID Image1 Default UI Material -1276 11182 Text1 Default UI Material -1276 522 Image2 Default UI Material -1276 11188 Text2 Default UI Material -1276 522 Image3 MaterialTest 2944 11182 - 对Depth进行升序排列。
节点名称 Depth排序前 节点名称 Depth排序后 Image1 0 Image1 0 Text1 1 Text1 1 Image2 1 Image2 1 Text2 2 Image3 1 Image3 1 Text2 2 - 对MatID进行升序排列,注意此时排序只针对相同的Depth值。
节点名称 Depth值 MatID排序前 节点名称 MatID排序后 Image1 0 -1276 Image1 -1276 Text1 1 -1276 Text1 -1276 Image2 1 -1276 Image2 -1276 Image3 1 2944 Image3 2944 Text2 2 -1276 Text2 -1758 - 对TextureID进行升序排列,注意此时排序只针对相同的Depth和MatID。
节点名称 Depth值 MatID值 TextureID排序前 节点名称 TextureID排序后 Image1 0 -1276 11182 Image1 11182 Text1 1 -1276 522 Text1 522 Image2 1 -1276 11188 Image2 11188 Image3 1 2944 11182 Image3 11182 Text2 2 -1758 522 Text2 522 - 最后将Depth、MatID、TextureID相同的节点进行合并,很明显没有能够合并的节点,最终得到了5个DrawCall,它们的先后顺序是Image1->Text1->Image2->Image3->Text2。
- Frame Debug的结果
Pos.z不等于0的情况
当Pos.z不等于0时,满足一下条件就可以合批:
- 图集和材质一样
- 在Hierarchy中是相邻的,可以是父子节点
- 跟Depth值以及是否与别的节点相交没有关系
优化点
- 一个Canvas下的元素才会合批,不同Canvas,即使Order in Layer相同也不会合批。
- Tag、Layer不同,是否添加了outline、shadow脚本,都不会影响合批。
- 不使用UnityWhite等默认的Unity图片,比如:Mask、Sprite、Background等。
- 有时候为了合并层级,比如Text,就需要给Text垫高层级,即将Text下面放一个其他图集中的透明图片,从而使得层级整齐,以达到该Text与其他Text层级相同而能合批的目的。
Mask & RectMask2D
二者同是作为裁剪,但是原理和计算DrawCall的方式完全不同。
Mask
- Mask内的元素都不会跟外界非Mask内的元素合批。
- Mask内的元素可以和其他Mask内的元素合批。
- Mask会额外生成2个DrawCall。
从上图中可以得知:
- ScrollView中的Mask与其他ScrollView中的Mask是可以合批的。
- 一共产生5个DrawCall,依次是Background图片、UIMask、图片、字体和UIMask。
- 挂Mask与不挂Mask的区别在于材质球发生了变化,所以其实还是遵循基本的DrawCall计算规则,只不过不容易被合批。
- 尽量让Mask放在一个Canvas中,使其不影响外面的合批。
RectMask2D
- RectMask2D内的元素合批规则跟正常的是一样的。
- RectMask2D内的元素不会跟外界任何元素进行合批(即使是其他RectMask2D内的元素)。
- RectMask2D不会像Mask一样产生额外的DrawCall。
Mask与RectMask2D的区别
- Mask之所以会多出两个DrawCall,是因为Mask的原理是GPU的Shader实现的,第一个Mask是一个在底层模板绘制一个区域的命令,根据Image传进来的图片Alpha值,确定裁剪区域,之后Mask节点下的元素会根据这个区域计算Alpha的值,最后一个Mask是绘制区域结束的指令,用于结束计算裁剪的操作。
- RectMask2D不需要Image图片作为裁剪区域,所以它的裁剪区域就是矩形大小,进而CPU计算元素是否在矩形区域之内,如果在区域之内,则节点常规方式合批之后进行顶点裁剪,而如果一个元素完全不在矩形区域内,则这个元素不会被渲染。因此将一个元素完全脱离矩形区域外,这个节点的DrawCall变为0,而Mask却没有这个现象。
- Mask和RectMask2D的本质区别是GPU实现还是CPU实现。